Java面向对象(三) 继承和final实例

Java面向对象(三) 继承和final实例

继承

概述

  • 继承是面向对象思想的三大特性之一,使类与类之间产生特殊 - 一般的关系,即is-a关系。
  • 继承是从已有类中派生出新的类,新的类能吸收已有类的属性和方法,并且能拓展新的属性和行为
  • 在Java中使用extends关键字表示继承,语法表示为: class 子类 extends 父类{}
  • 子类被称为派生类,父类又被称为超类
  • 子类继承父类,表名子类是一种特殊的父类,子类拥有父类的属性和方法,并且子类可以拓展具有父类所没有的一些属性和方法。
  • 子类即是不扩展父类,也能维持拥有父类的操作。

优缺点

  • 继承的好处
    • 提高了代码的复用性
    • 提高了代码的维护性
    • 让类与类之间产生了关系,是多态的前提
  • 缺点
    • 增加了耦合性
  • OOP思想开发原则:高内聚,低耦合
    • 耦合:类与类之间的关系
    • 内聚:自身完成事情的能力

Java继承特点

  • Java只支持单继承不支持多重继承操作(extends A,B,C..)

    1
    2
    3
    class A {}
    class B {}
    class C extends A,B {} // 错误的,一个子类继承了两个父类,Java中不允许
  • 为什么只支持单继承?

    • 多继承会存在安全隐患,因为当继承的多个类都存在相同的属性方法名相同方法体不同的方法,子类进行调用时,就会产生不知道该调用哪一个类中的方法的情况。
  • Java支持多层继承(继承体系)

    1
    2
    3
    class A {}
    class B extends A {}
    class C extends B {}
  • 如果想用这个继承体系的所有功能,那么就实用对底层的子类创建的对象

  • 如果想看这个体系的共性功能,那么就看最顶层的类的功能

继承注意点

  1. 成员变量和方法

    • 子类只能继承父类的所有非私有的成员变量和方法。可以继承public protected 修饰的成员,不可以继承private修饰的。
    • 但是可以通过父类中提供的public 的setter和getter方法进行间接访问和操作private 的属性
    • 对于子类可以继承父类中的成员变量和成员方法,如果子类出现和父类同名的成员变量和成员方法时,父类成员变量会被隐藏,父类的成员方法会被覆盖。需要使用父类的成员变量和方法时,就需要使用super关键字来进行引用
      • 隐藏是针对成员变量和静态方法,覆盖是针对普通方法。
  1. 构造器

    • 子类不能继承父类的构造方法,但是可以通过super关键字来访问父类构造方法。

    • super 和 this 的调用都必须是在第一句,否则会产生编译错误,this和super只能存在一个。

    • 不能进行递归构造器调用,即多个构造器之间互相循环调用。

    • 如果父类有无参构造时,所有构造方法(包含任意有参构造)自动默认都会访问父类中的空参构造方法。(自带super();

      • 因为继承的目的是子类获取和使用父类的属性和行为,所以子类初始化之前,一定要先完成父类数据的初始化。
      • 在Java中,每个类都会默认继承Object超类,所以每一个构造方法的第一条默认语句都是super()
    • 如果父类没有无参构造,反而有其他的有参构造方法时,子类继承父类后,子类必须显式的创建构造器,不论子类的构造器是否和父类构造器中参数类型是否一致,都必须在子类的构造器中显式的通过super关键字调用和父类构造器相应参数的构造方法。否则编译都通不过。代码示例如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Person {
      public Person(int age){
      System.out.println(age);
      }

      }
      class Student extends Person{
      public Student(int age) {
      super(age);
      }
      public Student(){
      super(10); //必须调用父类的有参构造
      System.out.println("子类可以创建其他类型构造器,但是必须显式的用super调用父类构造器")
      }
      }

      也可以使用this先调用子类中的构造方法,再间接调用父类中的有参构造方法,实例如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public class ExtendTest1  {
      public static void main(String[] args) {
      new Student();
      }

      }
      class Person {
      public Person(int age){
      System.out.println("父类有参构造");
      }

      }
      class Student extends Person{
      public Student(int age) {
      super(age);
      System.out.println("子类有参构造");
      }
      public Student(){
      this(10); //可以使用this先调用子类中的有参构造,从而间接调用父类中的有参构造
      System.out.println("子类无参构造");
      }
      }

      使用this,执行顺序结果为:先调用了子类中无参构造,此无参构造会接着调用子类中的有参构造,又接着调用父类中的有参构造,此时首先执行完毕了父类有参构造,接着子类有参构造执行完毕,最后子类无参构造才执行完毕。

      1
      2
      3
      父类有参构造
      子类有参构造
      子类无参构造
以下这种是错误的:(因为当父类中没有无参构造器时,父类中没有这种类型的构造方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Student extends Person{
public Student(String name){
super();
} //错误的,因为当父类中没有无参构造器时,父类中没有这种类型的构造方法
public Student(int age) {
super(age);
}
}

class Person {
public Person(String name ,int age){
System.out.println(name+age);
}
public Person(int age){
System.out.println(age);
}
}
以下这种正确:(因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student extends Person{
//因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有
public Student(int age) {
super(age);
}
}

class Person {
public Person(String name ,int age){
System.out.println(name+age);
}
public Person(int age){
System.out.println(age);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student extends Person{
//因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有
public Student(String name ,int age){
super(name,age);
}
public Student(int age) {
super(age);
}
}

class Person {
public Person(String name ,int age){
System.out.println(name+age);
}
public Person(int age){
System.out.println(age);
}
}
结论:**当父类中没有无参构造器时,子类继承父类,子类中的构造器方法类型可以和父类中的构造器不同,但是必须每个构造器都显式的使用super关键字调用父类中的某个有参构造器,也可以使用this调用子类中的某个有参构造器,但这个有参构造器必须通过super访问父类中的有参构造器。**

继承的执行顺序问题

  • 父类和子类中都有静态代码块和构造代码块,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class Test2_Extends {
    public static void main(String[] args) {
    Zi z = new Zi();
    }
    }
    class Fu {
    static {
    System.out.println("静态代码块Fu");
    }

    {
    System.out.println("构造代码块Fu");
    }

    public Fu() {
    System.out.println("构造方法Fu");
    }
    }

    class Zi extends Fu {
    static {
    System.out.println("静态代码块Zi");
    }

    {
    System.out.println("构造代码块Zi");
    }

    public Zi() {
    System.out.println("构造方法Zi");
    }
    }

    此时的执行结果:

    1
    2
    3
    4
    5
    6
    静态代码块Fu
    静态代码块Zi
    构造代码块Fu
    构造方法Fu
    构造代码块Zi
    构造方法Zi

    执行顺序分析:

    1. 主类Test2_Extends先加载到内存,其中的main方法入栈执行,main方法中创建了子类对象
    2. 子类对象创建过程中,父类和子类都加载到内存中,并且Fu.class优先于Zi.class加载,父类中的静态域先执行后,再执行子类中的静态域,此时会第一个输出:静态代码块Fu,第二个输出:静态代码块Zi
    3. 创建对象时进入子类的构造器,因为Java是分层初始化的,所以会先初始化父类再初始化子类,子类构造器会自动默认先执行父类的构造器,因为构造代码块优先于构造方法执行,所以此时就会先执行父类的构造代码块后,再执行父类的构造方法。所以第三个输出:构造代码块Fu,第四个输出:构造方法Fu
    4. Fu类初始化结束后,子类初始化,第五个输出的是:构造代码块Zi,第六个输出:构造方法Zi

方法重写

  • 重写:子父类出现一模一样的方法,但返回值类型可以是子父类
  • 方法重写的应用:
    • 当子类需要父类的功能,而功能主体子类有自己的特有内容时,可以重写父类中的方法。即沿用了父类的功能,又定义了子类特有的内容

重写注意点

  • 父类中的私有方法不能被重写
    • 因为父类的私有方法子类无法继承,得不到
  • 子类重写父类方法时,访问权限不能更低,子类方法访问权限应该大于或等于父类的,最好和父类中一致
  • 父类静态方法,子类也必须通过静态方法进行覆盖,即静态只能覆盖静态
  • 子类重写父类方法时,最好声明得一模一样

重写和重载

  • OverrideOverload 的区别?Overload能改变返回值类型吗?
    • Override是重写Overload是重载重载可以改变返回值类型,它是方法名相同,参数列表不同,与返回值类型无关
  • 方法重写:子类中出现和父类中方法声明一模一样的方法返回值类型相同(或者是子父类,多态),方法名和参数列表一模一样
  • 方法重载:本类中出现方法名相同参数列表不同的方法,和返回值类型无关,可以改变
  • 子类对象调用方法时,先找子类本身的方法,再找父类中的方法。

final实例域

  • 可以将实例域定义为final构造对象时必须初始化这样的域
  • 必须确保在每一个构造器执行之后,这个域的值被设置初始化,并且在后面的操作中,不能够再对它进行修改
  • final修饰的大都应用于基本类型域,或不可变类的域
    • 不可变类:如果类中的每个方法都不会改变其对象,这种类是不可变的类。例如String类。
  • 对于可变类,使用final修饰只是表示存储在对象变量中的对象引用不会再指向其他对象,不过这个对象中的属性可以更改

final修饰特点

  • 修饰时,类不能被继承
  • 修饰变量时,变量就变成了常量,只能被初始化赋值一次,后续不可进行改变。一般与 public static 配合使用。
  • 修饰方法时,方法不能被重写
  • final修饰局部变量时:
    • 方法内部或者方法声明上
    • 基本类型:值不能被改变
    • 引用类型地址引用不能被改变,不会再指向其他对象,但是对象中的属性可以改变。会报 “无法为最终变量x分配值” 的错误

final变量的初始化时机

  • final修饰的成员变量必须显式的进行初始化赋值,否则默认的是个无效值,会报“可能尚未初始化变量xxx”的错误。

    1
    2
    3
    4
    5
    6
    7
    //方式1:直接进行显式得初始化赋值,不进行赋值,会报错
    Class Demo{
    final int num ; //没有显式赋值,报错
    final int num = 10//进行显式得初始化赋值
    public Demo(){
    }
    }
  • 也可以在构造方法执行完毕前对其进行初始化赋值
1
2
3
4
5
6
7
//方式2:在构造方法执行完毕之前进行赋值初始化
Class Demo{
final int num;
public Demo(){
num = 10; //在构造方法执行完毕之前进行赋值初始化
}
}